/**
 *  JSimProd - Java simulation code for production processes
 *  Copyleft Jorge Mira Yagüe <jorge.mira.yague@gmail.com>
 *  Universidad de Alicante 2009
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package lib.simul;

import java.util.Calendar;
import java.util.List;
import lib.exceptions.InvalidLengthException;
import lib.exceptions.NonExistentStationException;
import lib.exceptions.OverlappedIntervalException;
import lib.model.Interval;
import lib.model.FactoryModel;
import lib.model.ItemCategory;
import lib.model.Station;
import lib.model.queue.QueueType;
import lib.plan.Item;
import lib.plan.Order;
import lib.plan.ProductionPlan;
import lib.xml.XMLizable;

/**
 * The class Run allows to create agendas from a FactoryModel and a
 * ProductionPlan.  A Run object may be paused and resumed at any moment while
 * running, and it could be also stopped without any chance to resume it from
 * the point it was when stopped.
 * @see Agenda
 * @see FactoryModel
 * @see ProductionPlan
 * @author Jorge Mira Yagüe
 */
public class Run implements XMLizable {

    private Agenda agenda;
    private FactoryModel factory;
    private Station source;
    private Station sink;
    private boolean pause;
    private boolean started;
    private int actualIndex;

    /**
     * Builds a run with the factory model given
     * @param factory the factory model on wich the run is going to be executed
     * @see FactoryModel
     */
    public Run(FactoryModel factory) {
        this.factory = factory;
        this.agenda = new Agenda();
        actualIndex = 0;
        started = false;
    }

    private void CreateSource(ProductionPlan plan, Interval interval) {
        List<Order> orders;
        List<Item> items;
        Event event;
        Calendar date;

        source = new Station("Start", QueueType.FIFO);
        orders = plan.getOrders();

        for (Order order : orders) {
            if (interval.contains(order.getDateReceived())) {
                items = order.getItems();
                for (Item item : items) {
                    item.flush();
                    date = (Calendar) order.getDateReceived().clone();
                    event = new Event(date,
                            EventType.OBJECT_DEPARTURE,
                            item,
                            source.getName());
                    agenda.add(event);

                }
            }
        }
    }

    private void CreateSink() {
        sink = new Station("End", QueueType.FIFO);
    }

    /**
     * Creates an agenda with the parameters given
     * @param plan the production plan to build the agenda
     * @param interval the interval on which the agenda will be built, only
     * events contained on the interval will be added
     * @see ProductionPlan
     * @see Interval
     */
    public void CreateAgenda(ProductionPlan plan, Interval interval)
            throws InvalidLengthException,
            OverlappedIntervalException,
            NonExistentStationException {

        Event actualEvent;
        Event nextEvent;
        Event queuedEvent;
        Item actualItem;
        Item polledItem;
        Station actualStation;
        Station nextStation;
        ItemCategory itemCategory;
        Calendar actualCalendar;
        Calendar nextCalendar;
        String stationName;
        List<String> stations;
        Interval job;

        if (!started) {
            CreateSource(plan, interval);
            CreateSink();
            started = true;
        }

        while (actualIndex < agenda.size() && !pause) {
            actualEvent = agenda.getEvent(actualIndex);
            if (actualEvent.getStationName().equals(sink.getName())) {
                actualIndex++;
                continue;
            }
            if (actualEvent.getStationName().equals(source.getName())) {
                actualStation = source;
            } else {
                actualStation = factory.getStation(actualEvent.getStationName());
            }
            actualItem = actualEvent.getItem();
            actualCalendar = agenda.nextFreeSlot(actualEvent.getCalendar());
            if (interval.contains(actualCalendar)) {
                switch (actualEvent.getType()) {
                    case OBJECT_ARRIVAL:
                        actualStation.QueueItem(actualItem);
                        if (!actualStation.isBooked()) {
                            polledItem = actualStation.pollItem();
                            if (polledItem != null) {
                                nextEvent = new Event(actualCalendar,
                                        EventType.START_SERVICE,
                                        polledItem,
                                        actualStation.getName());
                                agenda.add(nextEvent);
                                actualStation.setBooked(true);
                            }
                        }
                        break;

                    case START_SERVICE:
                        job = new Interval(actualCalendar,
                                actualItem.getActualParameter().getResult());
                        itemCategory = factory.getItemCategory(
                                actualItem.getCategory());
                        nextCalendar = actualStation.addJob(job,
                                itemCategory.getTask(
                                actualItem.getTaskNum()).getSameShift());
                        if (interval.contains(nextCalendar)) {
                            nextEvent = new Event(nextCalendar,
                                    EventType.END_SERVICE,
                                    actualItem,
                                    actualStation.getName());
                            agenda.add(nextEvent);
                        }
                        break;

                    case END_SERVICE:
                        nextEvent = new Event(actualCalendar,
                                EventType.OBJECT_DEPARTURE,
                                actualItem,
                                actualStation.getName());
                        agenda.add(nextEvent);

                        actualCalendar = (Calendar) actualCalendar.clone();
                        polledItem = actualStation.pollItem();
                        if (polledItem != null) {
                            queuedEvent = new Event(actualCalendar,
                                    EventType.START_SERVICE,
                                    polledItem,
                                    actualStation.getName());
                            agenda.add(queuedEvent);

                        } else {
                            actualStation.setBooked(false);
                        }
                        actualStation.decreaseQueueLength();
                        break;

                    case OBJECT_DEPARTURE:
                        if (!actualEvent.getStationName().equals(source.getName())) {
                            actualItem.increaseTaskNum();
                        }

                        if (actualItem.isFinished()) {
                            stationName = sink.getName();
                        } else {
                            itemCategory = factory.getItemCategory(
                                    actualItem.getCategory());
                            stations = itemCategory.getTask(
                                    actualItem.getTaskNum()).getStations();
                            stationName = bestStation(stations);
                            if (stationName == null) {
                                throw new NonExistentStationException();
                            }
                            nextStation = factory.getStation(stationName);
                            nextStation.increaseQueueLength();
                        }
                        nextEvent = new Event(actualCalendar,
                                EventType.OBJECT_ARRIVAL,
                                actualItem,
                                stationName);
                        agenda.add(nextEvent);
                        break;

                    default:
                        break;
                }
            }
            actualIndex++;
        }
    }

    public Agenda getAgenda() {
        return agenda;
    }

    public boolean isPaused() {
        return pause;
    }

    /**
     * Pauses the simulation if running or starts it again if paused
     */
    public void Pause() {
        pause = !pause;
    }

    private String bestStation(List<String> stations) {
        Station station;
        String winnerStation = null;
        int minLength = Integer.MAX_VALUE;
        int actualLength;

        for (String stationName : stations) {
            station = factory.getStation(stationName);
            actualLength = station.getQueueLength();
            if (actualLength < minLength) {
                minLength = actualLength;
                winnerStation = station.getName();
            }
        }

        return winnerStation;
    }

    public String toXML() {
        String result = "";
        result += "<Run>\n";
        result += agenda.toXML();
        result += "</Run>\n";
        return result;
    }
}
